package NG::Sys::Cached::List;
use strict;
use warnings;
use utf8;

use JSON::XS;

use NG::Sys::Cached::Common;
our @ISA = qw(NG::Sys::Cached::Common::Parent);

require NG::Sys::Cached::List::Object;
require NG::Sys::Cached::List::Record;

# pkey - ключ группировки списка
# ekey - уникальный ключ элемента списка
# summary - признак хранения сумаррной информации по списку
# object_class - класс реализующий методы списка
# record_class - класс реализующий методы элемента списка
# fields -  список полей элементов списка
# blobs - список полей элементов, которые нужно сеарилизовать
sub init {
	my $self = shift;
	$self->SUPER::init;
	unless($self->{pkey}){
		die "need pkey\n";
	}
	unless($self->{ekey}){
		die "need ekey\n";
	}
	unless($self->{fields}){
		die "need fields\n";
	}
	if(my $s = $self->{summary}){
		$self->{s_cache} = $self->{cache}->clone($self->{namespace}.":summary");
		if($s eq "compress"){
			$self->{s_compress} = 1;
		}
	}

	if($self->make_fields and $self->{blobs}){
		die "cannot blobs without fields\n";
	}

	$self->{object_class} ||= 'NG::Sys::Cached::List::Object';
	$self->{record_class} ||= 'NG::Sys::Cached::List::Record';
	
	# массив для внешних объектов
	$self->{_extern} = [];
	# field -> pos in $self->{_extern}[]
	$self->{hash_extern} = {};

	Extend::soft_require($self->{record_class});
	Extend::soft_require($self->{object_class});

	if($self->{subclass}){
		$self->{record_class} = $self->make_subclass($self->{record_class},"rec");
		$self->{object_class} = $self->make_subclass($self->{object_class},"obj");
		if($self->{compile}){
			no strict 'refs';
			my $ekey = $self->{ekey};
			*{$self->{record_class}."::entry"} = eval "sub {
				my \$self = shift;
				return \$self->[2] ||= \$self->[0]{$ekey};
			}";
			*{$self->{record_class}."::remove"} = eval "sub {
				my \$self = shift;
				return \$self->[1]->update_data(\$self,'$ekey',0);
			}";
			*{$self->{record_class}."::__extern"} = sub {
				die "never called";
			};
			for my $key(@{$self->{fields}}){
				next if $self->{record_class}->can($key);
				*{$self->{record_class}."::".$key} = eval "sub {
					my \$self = shift;
					if(\@_){
						return \$self->[1]->update_data(\$self,'$key',\@_);
					}else{
						return \$self->[0]{$key};
					}
				}";
			}
		}
	}else{
		if(ref $self->{record_class}){
			die "cannot combine record_class without sublassing";
		}
		if(ref $self->{object_class}){
			die "cannot combine object_class without sublassing";
		}
		if($self->{compile}){
			die "need subclassing for compile";
		}
	}

	for my $create_param(@{$self->{extern}}){
		$self->add_extern($create_param);
	}

	return $self;
}
sub add_extern {
	my $self = shift;
	my $create_param;
	if(ref $_[0]){
		$create_param = shift;
	}elsif(@_ % 2){
		die "odd number params\n";
	}else{
		$create_param = { @_ };
	}
	
	unless($create_param->{class}){
		die "need class\n";
	}

	Extend::soft_require($create_param->{class});
	
	unless($create_param->{namespace} ||= $create_param->{table}){
		die "need namespace or table class='$create_param->{class}'\n";
	}
	unless(exists $create_param->{cache}){
		$create_param->{cache} = $self->{cache};
	}
	$create_param->{app} = $self->{app};
	$create_param->{dbh} = $self->{dbh};
	
	push @{$self->{_extern}},$create_param;
	my $pos = $#{$self->{_extern}};
	my $field = $create_param->{namespace};
	
	if(defined $self->{hash_extern}{$field}){
		die "intercross extern '".$field."'\n";
	}
	
	$self->{hash_extern}{$field} = $pos;
	
	if(exists $self->{table_pos}{$create_param->{namespace}}){
		die "redefine extern '$create_param->{namespace}' with namespace";
	}

	$create_param->{object} = $create_param->{class}->new(%$create_param);
	
	if($self->{compile} && $self->{subclass}){
		no strict 'refs';
		my $root = $create_param->{object};
		my $ekey = $self->{ekey};
		*{$self->{record_class}."::".$field} = eval "sub {
			my \$self = shift;
			return \$self->[3][$pos] ||= \$root->get_obj(\$self->[0]{$ekey},\$self->[1]{readonly});
		}";
	}
}
sub need_create {
	return undef;
}
sub make_fields {
	my $self = shift;
	@{$self->{hash_fields}}{@{$self->{fields}}} = (1) x @{$self->{fields}};
	if($self->{blobs}){
		$self->{hash_blobs} = { map {$_=>1} $self->check_fields_ary($self->{blobs}) };
	}
	if($self->{hash_fields}{$self->{ekey}}){
		die "ekey in fields\n";
	}
	unless($self->{hash_fields}{$self->{pkey}}){
		die "pkey not in fields\n";
	}
	if($self->{ikey} && !$self->{hash_fields}{$self->{ikey}}){
		die "ikey not in fields\n";
	}
	return 0;
}
sub check_field {
	my $self = shift;
	my $key = shift;
	return exists $self->{hash_fields}{$key} || die "unexists key'".$key."' checked\n";
}
sub check_fields_ary {
	my ($self,$keys) = @_;
	my $h = $self->{hash_fields};
	exists $h->{$_} || die "unexists key'".$_."' checked\n" for @$keys;
	return @$keys;
}

sub _prepare_for_store {
	my $self = shift;
	my ($data,$keys) = @_;
	unless($keys){
		$keys = $self->{fields};
		for(@$keys){
			unless(exists $data->{$_}){
				$data->{$_} = undef;
			}
		}
	}
	my %ndata;
	if(my $hash_blobs = $self->{hash_blobs}){
		for(@$keys){
			my $v = $data->{$_};
			$ndata{$_} = $hash_blobs->{$_} && ref $v ? encode_json($v) : $v;
		}
	}else{
		@ndata{@$keys} = @{$data}{@$keys};
	}
	$ndata{$self->{ekey}} = $data->{$self->{ekey}};
	return \%ndata;
}
#return always [] or die
sub _read_source {
	#my $self = shift;
	#my $id = shift;
	die "call virtual method '_read_source'\n";
}
sub _update_source {
	#my $self = shift;
	#my ($data,$keys) = @_;
	die "call virtual method '_update_source'\n";
}
sub _delete_source {
	#my $self = shift;
	#my $entry = shift;
	die "call virtual method '_delete_source'\n";
}
sub _delete_source_ary {
	#my $self = shift;
	#my $entryes = shift;
	die "call virtual method '_delete_source'\n";
}
sub _remove_source {
	#my $self = shift;
	#my $pkey = shift;
	die "call virtual method '_remove_source'\n";
}
sub _insert_source {
	#my $self = shift;
	#my $row = shift;
	die "call virtual method '_insert_source'\n";
}

sub get_obj {
	my $self = shift;
	my $id = shift;
	my $readonly = shift;
	return bless {parent=>$self,id=>$id,readonly => $readonly},$self->{object_class};
}
sub _remove_data {
	my $self = shift;
	my $object = shift;

	if(@{$self->{_extern}}){
		my $data = $self->_read_data($object,1);
		my $ekey = $self->{ekey};
		for my $row( @{$data->[0]} ){
			for my $ext(@{$self->{_extern}}){
				my $obj = $ext->{object}->get_obj($row->{$ekey});
				$obj->remove;
			}
		}
	}
	
	$self->_remove_source($object->{id});
	$self->clear_cache($object->{id});
}
sub _read_data {
	my $self = shift;
	my $object = shift;
	my $for_store = shift;
	my $id = $object->{id};
	my $data;
	if($data = $self->context->{$id}){
		$self->count_action("context");
		return $data;
	}
	if($data = $self->{cache}->get($id)){
		$self->count_action("cache");
		if($self->{compress}){
			$data = Compress::Zlib::memGunzip($data);
		}
		$data = decode_json($data);
	}elsif($data = $self->_read_source($id)){
		$self->count_action("source");
		my %index;
		my %index2;
		my $length = 0;
		my $ekey = $self->{ekey};

		my $blobs = $self->{blobs};
		my $ikey = $self->{ikey};
		for my $row(@$data){
			if($blobs){
				defined $_ and $_ = decode_json($_) for @{$row}{@$blobs};
			}
			if($ikey){
				push @{$index2{$row->{$ikey}} ||= []}, $length;
			}
			$index{$row->{$ekey}} = $length++;
		}

		$data = [$data,\%index,$length,\%index2];
		# если данные получаем для записи, то кэш не пишем,
		# так как он будет записан, после изменений
		unless($for_store){
			my $fdata = encode_json($data);	
			if($self->{compress}){
				$fdata = Compress::Zlib::memGzip($fdata);
			}
			$self->{cache}->set($id,$fdata);
		}else{
			$data->[4] = 1;
		}
	}else{
		$data = [[],{},0,{}];
	}
	if($data){
		#$data = $self->_prepare_list($object,$data);
		return $self->context->{$id} = $data;
	}else{
		return undef;
	}
}

# list - это или строка для изменений
# или скаляр, указывающий , что список строк для изменений идет следующим параметром
sub _update_data {
	my $self = shift;
	my $object = shift;
	my $list = shift;
	if(ref $list){
		$list = [ $list ];
	}else{
		$list = shift;
	}
	#print Data::Dumper::Dumper($list);
	my (%list_remove, # список для удаления
	   %list_change, # список для изменения
	   %rec_remove,  # список для 
	   @add_list,  # список для добавления
	);

	my $ekey = $self->{ekey};
	my $pkey = $self->{pkey};
	my $ikey = $self->{ikey};

	my $data = $self->_read_data($object,1);

	for my $rid (@$list){
		my($id,$key,$val) = @$rid;
		# если id  нет, то значит надо вставлять и вся строка целиком идет в $key
		unless($id){
			push @add_list,$key;
			next;
		}
		
		if($key eq $pkey){
			die "trying change pkey ",
				"ns='$self->{namespace}' obj='$object->{id}' rec='$id' ",
				"to '",(defined $val?$val:'<undef>'),"'\n";
		}elsif($key eq $ekey){
			# если меняем ekey значит удаляем данный элемент списка
			# но обязательно Val  должен быть == 0
			if($val){
				die "trying change ekey ",
					"ns='$self->{namespace}' obj='$object->{id}' rec='$id' ",
					"to '",(defined $val?$val:'<undef>'),"'\n";
			}else{
				$rec_remove{$id} = 1;
			}
		}else{
			my $pos = $data->[1]{$id};
			if(($list_change{$pos}{$key}||=0)++){
				die "double change in one transaction ",
					"ns='$self->{namespace}' obj='$object->{id}' rec='$id' key='$key' ",
					"to '",(defined $val?$val:'<undef>'),"'\n";
			}
			$data->[0][$pos]{$key} = $val;
		}
	}
	my $recalc;
	my $reindex;

	while(my($pos,$keys) = each %list_change){
		my $rec = $data->[0][$pos];
		next if $rec_remove{$rec->{$ekey}};
		my $lkeys = [keys %$keys];
		$self->_update_source($self->_prepare_for_store($rec,$lkeys),$lkeys);
		$self->count_action("store");
		$recalc = 1;
		if($ikey && exists $keys->{$ikey}){
			$reindex = 1;
		}
	}
	if(scalar %rec_remove){
		$recalc = 1;
		$reindex = 1;
		my @entry_for_remove = keys %rec_remove;

		for my $entry( @entry_for_remove ){
			for my $ext(@{$self->{_extern}}){
				my $obj = $ext->{object}->get_obj($entry);
				$obj->remove;
			}
		}
		if(@entry_for_remove == 1){
			splice @{$data->[0]}, $data->[1]{$entry_for_remove[0]},1;
			$self->_delete_source($entry_for_remove[0]);
			$self->count_action("delete");
		}else{
			for my $pos (sort {$b <=> $a} @{$data->[1]}{@entry_for_remove}){
				splice @{$data->[0]}, $pos, 1;
				$self->count_action("delete");
			}
			$self->_delete_source_ary(\@entry_for_remove);
		}
	}
	my $id;
	for my $row(@add_list){
		$row->{$pkey} = $object->{id};
		if($id = $self->_insert_source($self->_prepare_for_store($row))){
			$row->{$ekey} = $id;
			push @{$data->[0]}, $row;
			$self->count_action("insert");
		}
		$recalc = 1;
		$reindex = 1;
	}
	if($recalc or $data->[4]){
		delete $data->[4];
		if($reindex){
			my $length = 0;
			my %index;
			my %index2;
			for my $row (@{$data->[0]}){
				push @{$index2{$row->{$ikey}} ||= []}, $length if $ikey;
				$index{$row->{$ekey}} = $length++;
			}
			$data->[1] = \%index;
			$data->[2] = $length;
			$data->[3] = \%index2;
		}
		my $oid = $object->{id};
		my $fdata = encode_json($data);
		if($self->{compress}){
			$fdata = Compress::Zlib::memGzip($fdata);
		}
		$self->{cache}->set($oid,$fdata);
		$self->context->{$oid} = $data;
		if($self->{summary}){
			$self->{s_cache}->remove($oid);
			delete $self->context->{"s:".$oid}
		}
	}
	return $id;
}
sub _summary {
	my $self = shift;
	unless($self->{summary}){
		die "need enable summary";
	}
	my $object = shift;
	my $id = $object->{id};
	my $data;
	if($data = $self->context->{"s:".$id}){
		$self->count_action("s_context");
		return $data;
	}
	if($data = $self->{s_cache}->get($id)){
		$self->count_action("s_cache");
		if($self->{s_compress}){
			$data = Compress::Zlib::memGunzip($data);
		}
		$data = decode_json($data);
	}elsif($data = $self->_read_data($object)){
		$self->count_action("s_calc");
		$data = $self->_make_summary($object,$data);
		my $fdata = encode_json($data);	
		if($self->{s_compress}){
			$fdata = Compress::Zlib::memGzip($fdata);
		}
		$self->{s_cache}->set($id,$fdata);
	}
	if($data){
		return $self->context->{"s:".$id} = $data;
	}else{
		return undef;
	}
}

sub drop_context {
	my $self = shift;
	my $id = shift;
	$self->count_action("drop");

	my $c = $self->context;

	my $data = delete $c->{$id};
	if($data && @{$self->{_extern}}){
		my $ekey = $self->{ekey};
		for my $row( @{$data->[0]} ){
			for my $ext(@{$self->{_extern}}){
				my $obj = $ext->{object}->get_obj($row->{$ekey});
				$obj->drop_context;
			}
		}
	}
	if($self->{summary}){
		delete $c->{"s:".$id};
	}
}
sub clear_cache {
	my $self = shift;
	my $id = shift;

	if(@{$self->{_extern}}){
		my $data;
		unless($data = $self->context->{$id}){
			if($data = $self->{cache}->get($id)){
				if($self->{compress}){
					$data = Compress::Zlib::memGunzip($data);
				}
				$data = decode_json($data);
			}
		}
		if($data){
			my $ekey = $self->{ekey};
			for my $row( @{$data->[0]} ){
				for my $ext(@{$self->{_extern}}){
					my $obj = $ext->{object}->get_obj($row->{$ekey});
					$obj->clear_cache;
				}
			}
			
		}
	}
	
	$self->{cache}->remove($id);
	$self->count_action("clear");

	my $c = $self->context;
	delete $c->{$id};

	if($self->{summary}){
		$self->{s_cache}->remove($id);
		delete $c->{"s:".$id};
	}
}

sub _make_summary {
	my $self = shift;
	my $object = shift;
	my $data = shift;
	return {};
}
1;
